﻿//=======================================================================================
// Microsoft Windows Server AppFabric Customer Advisory Team (CAT) Best Practices Series
//
// This sample is supplemental to the technical guidance published on the community
// blog at http://blogs.msdn.com/appfabriccat/. 
//
//=======================================================================================
// Copyright © 2010 Microsoft Corporation. All rights reserved.
// 
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER 
// EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF 
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. YOU BEAR THE RISK OF USING IT.
//=======================================================================================

using System;
using System.Threading;

namespace HashFoo.SqlServer.TransientDbConnection
{

    public class RetryCallbackInfo
    {
        public int CurrentRetryCount { get; set; }
        public Exception LastException { get; set; }
        public TimeSpan Delay { get; set; }
    }

    /// <summary>
    /// Provides the base implementation of the retry mechanism for unreliable actions and transient conditions.
    /// </summary>
    public abstract class RetryPolicy
    {
        /// <summary>
        /// The default number of retry attempts.
        /// </summary>
        public static readonly int DefaultClientRetryCount = 5;

        /// <summary>
        /// The default amount of time used when calculating a random delta in the exponential delay between retries.
        /// </summary>
        public static readonly TimeSpan DefaultClientBackoff = TimeSpan.FromSeconds(10.0);

        /// <summary>
        /// The default maximum amount of time used when calculating the exponential delay between retries.
        /// </summary>
        public static readonly TimeSpan DefaultMaxBackoff = TimeSpan.FromSeconds(30.0);

        /// <summary>
        /// The default minimum amount of time used when calculating the exponential delay between retries.
        /// </summary>
        public static readonly TimeSpan DefaultMinBackoff = TimeSpan.FromSeconds(1.0);

        /// <summary>
        /// The default amount of time defining an interval between retries.
        /// </summary>
        public static readonly TimeSpan DefaultRetryInterval = TimeSpan.FromSeconds(1.0);

        /// <summary>
        /// Policy that does no retries, it just invokes action exactly once.
        /// </summary>
        public static readonly RetryPolicy NoRetry = new RetryPolicy<TransientErrorIgnoreStrategy>(0);

        /// <summary>
        /// A boolean flag indicating whether or not the very first retry attempt will be made immediately
        /// whereas the subsequent retries will remain subject to retry interval.
        /// </summary>
        public bool FastFirstRetry { get; set; }

        /// <summary>
        /// An instance of a callback delegate which will be invoked whenever a retry condition is encountered.
        /// </summary>
        public event Action<RetryCallbackInfo> RetryOccurred;

        /// <summary>
        /// Repetitively executes the specified action while it satisfies the current retry policy.
        /// </summary>
        /// <param name="action">A delegate representing the executable action which doesn't return any results.</param>
        public abstract void ExecuteAction(Action action);

        /// <summary>
        /// Repetitively executes the specified action while it satisfies the current retry policy.
        /// </summary>
        /// <typeparam name="T">The type of result expected from the executable action.</typeparam>
        /// <param name="func">A delegate representing the executable action which returns the result of type T.</param>
        /// <returns>The result from the action.</returns>
        public abstract T ExecuteAction<T>(Func<T> func);

        /// <summary>
        /// Notifies the subscribers whenever a retry condition is encountered.
        /// </summary>
        /// <param name="retryCount">The current retry attempt count.</param>
        /// <param name="lastError">The exception which caused the retry conditions to occur.</param>
        /// <param name="delay">The delay indicating how long the current thread will be suspended for before the next iteration will be invoked.</param>
        protected virtual void OnRetryOccurred(int retryCount, Exception lastError, TimeSpan delay)
        {
            if (RetryOccurred != null) RetryOccurred(new RetryCallbackInfo
            {
                CurrentRetryCount = retryCount,
                LastException = lastError,
                Delay = delay
            });
        }

        /// <summary>
        /// Implements a strategy that ignores any transient errors.
        /// </summary>
        private sealed class TransientErrorIgnoreStrategy : ITransientErrorDetectionStrategy
        {
            public bool IsTransient(Exception ex)
            {
                return false;
            }
        }
    }

    /// <summary>
    /// Extends the base <see cref="RetryPolicy"/> implementation with strategy objects capable of detecting transient conditions.
    /// </summary>
    /// <typeparam name="T">The type implementing the <see cref="ITransientErrorDetectionStrategy"/> interface which is responsible for detecting transient conditions.</typeparam>
    public class RetryPolicy<T> : RetryPolicy
        where T : ITransientErrorDetectionStrategy, new()
    {
        private readonly T _errorDetectionStrategy = new T();
        private readonly ShouldRetry _shouldRetry;

        protected delegate bool ShouldRetry(int retryCount, Exception lastException, out TimeSpan delay);

        /// <summary>
        /// Initializes a new instance of the RetryPolicy class with default settings.
        /// </summary>
        private RetryPolicy()
        {
            FastFirstRetry = true;
        }

        /// <summary>
        /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and default fixed time interval between retries.
        /// </summary>
        /// <param name="retryCount">The number of retry attempts.</param>
        public RetryPolicy(int retryCount)
            : this(retryCount, DefaultRetryInterval) { }

        /// <summary>
        /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and time interval between retries.
        /// </summary>
        /// <param name="retryCount">The number of retry attempts.</param>
        /// <param name="retryInterval">The interval between retries.</param>
        public RetryPolicy(int retryCount, TimeSpan retryInterval)
            : this()
        {
            Guard.ArgumentNotNegativeValue(retryCount, "retryCount");
            Guard.ArgumentNotNegativeValue(retryInterval.Ticks, "retryInterval");

            if (0 == retryCount)
            {
                _shouldRetry = delegate(int currentRetryCount, Exception lastException, out TimeSpan interval)
                              {
                                  interval = TimeSpan.Zero;
                                  return false;
                              };
            }
            else
            {
                _shouldRetry = delegate(int currentRetryCount, Exception lastException, out TimeSpan interval)
                              {
                                  if (_errorDetectionStrategy.IsTransient(lastException))
                                  {
                                      interval = retryInterval;
                                      return (currentRetryCount < retryCount);
                                  }
                                  else
                                  {
                                      interval = TimeSpan.Zero;
                                      return false;
                                  }
                              };
            }
        }

        /// <summary>
        /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and back-off parameters for calculating the exponential delay between retries.
        /// </summary>
        /// <param name="retryCount">The number of retry attempts.</param>
        /// <param name="minBackoff">The minimum back-off time.</param>
        /// <param name="maxBackoff">The maximum back-off time.</param>
        /// <param name="deltaBackoff">The time value which will be used for calculating a random delta in the exponential delay between retries.</param>
        public RetryPolicy(int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff)
            : this()
        {
            Guard.ArgumentNotNegativeValue(retryCount, "retryCount");
            Guard.ArgumentNotNegativeValue(minBackoff.Ticks, "minBackoff");
            Guard.ArgumentNotNegativeValue(maxBackoff.Ticks, "maxBackoff");
            Guard.ArgumentNotNegativeValue(deltaBackoff.Ticks, "deltaBackoff");
            Guard.ArgumentNotGreaterThan(minBackoff.TotalMilliseconds, maxBackoff.TotalMilliseconds, "minBackoff");

            _shouldRetry = delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval)
                          {
                              if (_errorDetectionStrategy.IsTransient(lastException) && currentRetryCount < retryCount)
                              {
                                  var random = new Random();

                                  var delta = (int)((Math.Pow(2.0, currentRetryCount) - 1.0) * random.Next((int)(deltaBackoff.TotalMilliseconds * 0.8), (int)(deltaBackoff.TotalMilliseconds * 1.2)));
                                  var interval = (int)Math.Min(checked(minBackoff.TotalMilliseconds + delta), maxBackoff.TotalMilliseconds);

                                  retryInterval = TimeSpan.FromMilliseconds(interval);

                                  return true;
                              }

                              retryInterval = TimeSpan.Zero;
                              return false;
                          };
        }

        /// <summary>
        /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters defining the progressive delay between retries.
        /// </summary>
        /// <param name="retryCount">The number of retry attempts.</param>
        /// <param name="initialInterval">The initial interval which will apply for the first retry.</param>
        /// <param name="increment">The incremental time value which will be used for calculating the progressive delay between retries.</param>
        public RetryPolicy(int retryCount, TimeSpan initialInterval, TimeSpan increment)
            : this()
        {
            Guard.ArgumentNotNegativeValue(retryCount, "retryCount");
            Guard.ArgumentNotNegativeValue(initialInterval.Ticks, "initialInterval");

            _shouldRetry = delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval)
                          {
                              if (_errorDetectionStrategy.IsTransient(lastException) && currentRetryCount < retryCount)
                              {
                                  retryInterval = TimeSpan.FromMilliseconds(initialInterval.TotalMilliseconds + increment.TotalMilliseconds * currentRetryCount);

                                  return true;
                              }

                              retryInterval = TimeSpan.Zero;
                              return false;
                          };
        }

        /// <summary>
        /// Repetitively executes the specified action while it satisfies the current retry policy.
        /// </summary>
        /// <param name="action">A delegate representing the executable action which doesn't return any results.</param>
        public override void ExecuteAction(Action action)
        {
            var retryCount = 0;
            TimeSpan delay;

            for (; ; )
            {
                Exception lastError;

                try
                {
                    action();
                    return;
                }
                catch (RetryLimitExceededException limitExceededEx)
                {
                    // The user code can throw a RetryLimitExceededException to force the exit from the retry loop.
                    // The RetryLimitExceeded exception can have an inner exception attached to it. This is the exception
                    // which we will have to throw up the stack so that callers can handle it.
                    if (limitExceededEx.InnerException != null) throw limitExceededEx.InnerException;

                    return;
                }
                catch (Exception ex)
                {
                    lastError = ex;

                    if (!_shouldRetry(retryCount++, lastError, out delay)) throw;
                }

                // Perform an extra check in the delay interval. Should prevent from accidentally ending up with the value of -1 which will block a thread indefinitely. 
                // In addition, any other negative numbers will cause an ArgumentOutOfRangeException fault which will be thrown by Thread.Sleep.
                if (delay.TotalMilliseconds < 0) delay = TimeSpan.Zero;

                OnRetryOccurred(retryCount, lastError, delay);

                if (retryCount > 1 || !FastFirstRetry) Thread.Sleep(delay);
            }
        }

        /// <summary>
        /// Repetitively executes the specified action while it satisfies the current retry policy.
        /// </summary>
        /// <typeparam name="R">The type of result expected from the executable action.</typeparam>
        /// <param name="func">A delegate representing the executable action which returns the result of type R.</param>
        /// <returns>The result from the action.</returns>
        public override R ExecuteAction<R>(Func<R> func)
        {
            var retryCount = 0;
            var delay = TimeSpan.Zero;
            Exception lastError = null;

            for (; ; )
            {
                lastError = null;

                try
                {
                    return func();
                }
                catch (RetryLimitExceededException limitExceededEx)
                {
                    // The user code can throw a RetryLimitExceededException to force the exit from the retry loop.
                    // The RetryLimitExceeded exception can have an inner exception attached to it. This is the exception
                    // which we will have to throw up the stack so that callers can handle it.
                    if (limitExceededEx.InnerException != null) throw limitExceededEx.InnerException;
                    else return default(R);
                }
                catch (Exception ex)
                {
                    lastError = ex;

                    if (!_shouldRetry(retryCount++, lastError, out delay)) throw;
                }

                // Perform an extra check in the delay interval. Should prevent from accidentally ending up with the value of -1 which will block a thread indefinitely. 
                // In addition, any other negative numbers will cause an ArgumentOutOfRangeException fault which will be thrown by Thread.Sleep.
                if (delay.TotalMilliseconds < 0) delay = TimeSpan.Zero;

                OnRetryOccurred(retryCount, lastError, delay);

                if (retryCount > 1 || !FastFirstRetry) Thread.Sleep(delay);
            }
        }
    }
}